package br.com.scaled.audio;

import android.media.AudioFormat;
import android.media.AudioRecord;
import android.media.MediaRecorder;
import android.util.Log;

public class AudioReader {

	 // Debugging tag.
		private static final String TAG = "AudioReader";

		
		// ******************************************************************** //
		// Private Data.
		// ******************************************************************** //
	    
	    // Our audio input device.
	    private AudioRecord audioInput;

	    // Our audio input buffer, and the index of the next item to go in.
	    private short[][] inputBuffer = null;
	    private int inputBufferWhich = 0;
	    private int inputBufferIndex = 0;

	    // Size of the block to read each time.
	    private int inputBlockSize = 0;
	    
	    // Time in ms to sleep between blocks, to meter the supply rate.
	    private long sleepTime = 0;
	    
	    // Listener for input.
	    private Listener inputListener = null;
	    
	    // Flag whether the thread should be running.
	    private boolean running = false;
	    
	    // The thread, if any, which is currently reading.  Null if not running.
	    private Thread readerThread = null;
	    
	    /**
	     * Listener for audio reads.
	     */
	    public static abstract class Listener {
	        /**
	         * Audio read error code: no error.
	         */
	        public static final int ERR_OK = 0;
	        
	        /**
	         * Audio read error code: the audio reader failed to initialise.
	         */
	        public static final int ERR_INIT_FAILED = 1;
	        
	        /**
	         * Audio read error code: an audio read failed.
	         */
	        public static final int ERR_READ_FAILED = 2;
	        
	        /**
	         * An audio read has completed.
	         * @param   buffer      Buffer containing the data.
	         */
	        public abstract void onReadComplete(short[] buffer);
	        
	        /**
	         * An error has occurred.  The reader has been terminated.
	         * @param   error       ERR_XXX code describing the error.
	         */
	        public abstract void onReadError(int error);
	    }
	    
	    /**
		 * Create an AudioReader instance.
		 */
	    public AudioReader() {

	    }
	    
	    // ******************************************************************** //
	    // Run Control.
	    // ******************************************************************** //

	    /**
	     * Start this reader.
	     * 
	     * @param   rate        The audio sampling rate, in samples / sec.
	     * @param   block       Number of samples of input to read at a time.
	     *                      This is different from the system audio
	     *                      buffer size.
	     * @param   listener    Listener to be notified on each completed read.
	     */
	    public void startReader(int rate, int block, Listener listener) {
	        Log.i(TAG, "Reader: Start Thread");
	        synchronized (this) {
	            // Calculate the required I/O buffer size.
	            int audioBuf = AudioRecord.getMinBufferSize(rate,
	                                         AudioFormat.CHANNEL_CONFIGURATION_MONO,
	                                         AudioFormat.ENCODING_PCM_16BIT) * 2;

	            // Set up the audio input.
	            audioInput = new AudioRecord(MediaRecorder.AudioSource.MIC,
	                                         rate,
	                                         AudioFormat.CHANNEL_CONFIGURATION_MONO,
	                                         AudioFormat.ENCODING_PCM_16BIT,
	                                         audioBuf);
	            inputBlockSize = block;
	            sleepTime = (long) (1000f / ((float) rate / (float) block));
	            inputBuffer = new short[2][inputBlockSize];
	            inputBufferWhich = 0;
	            inputBufferIndex = 0;
	            inputListener = listener;
	            running = true;
	            readerThread = new Thread(new Runnable() {
	                public void run() { readerRun(); }
	            }, "Audio Reader");
	            readerThread.start();
	        }
	    }
	    
	    /**
	     * Stop this reader.
	     */
	    public void stopReader() {
	        Log.i(TAG, "Reader: Signal Stop");
	        synchronized (this) {
	            running = false;
	        }
	        try {
	            if (readerThread != null)
	                readerThread.join();
	        } catch (InterruptedException e) {
	           e.printStackTrace();
	        }
	        readerThread = null;
	        
	        // Kill the audio input.
	        synchronized (this) {
	            if (audioInput != null) {
	                audioInput.release();
	                audioInput = null;
	            }
	        }
	        
	        Log.i(TAG, "Reader: Thread Stopped");
	    }
	    
	    // ******************************************************************** //
	    // Main Loop.
	    // ******************************************************************** //
	    
	    /**
	     * Main loop of the audio reader.  This runs in our own thread.
	     */
	    private void readerRun() {
	        short[] buffer;
	        int index, readSize;
	        
	        int timeout = 200;
	        try {
	            while (timeout > 0 && audioInput.getState() != AudioRecord.STATE_INITIALIZED) {
	                Thread.sleep(50);
	                timeout -= 50;
	            }
	        } catch (InterruptedException e) {
	        	e.printStackTrace();
	        }

	        if (audioInput.getState() != AudioRecord.STATE_INITIALIZED) {
	            Log.e(TAG, "Audio reader failed to initialize");
	            readError(Listener.ERR_INIT_FAILED);
	            running = false;
	            return;
	        }

	        try {
	            Log.i(TAG, "Reader: Start Recording");
	            audioInput.startRecording();
	            while (running) {
	                long stime = System.currentTimeMillis();

	                if (!running)
	                    break;

	                readSize = inputBlockSize;
	                int space = inputBlockSize - inputBufferIndex;
	                if (readSize > space)
	                    readSize = space;
	                buffer = inputBuffer[inputBufferWhich];
	                index = inputBufferIndex;

	                synchronized (buffer) {
	                    int nread = audioInput.read(buffer, index, readSize);

	                    boolean done = false;
	                    if (!running)
	                        break;
	                    
	                    if (nread < 0) {
	                        Log.e(TAG, "Audio read failed: error " + nread);
	                        readError(Listener.ERR_READ_FAILED);
	                        running = false;
	                        break;
	                    }
	                    int end = inputBufferIndex + nread;
	                    if (end >= inputBlockSize) {
	                        inputBufferWhich = (inputBufferWhich + 1) % 2;
	                        inputBufferIndex = 0;
	                        done = true;
	                    } else
	                        inputBufferIndex = end;

	                    if (done) {
	                        readDone(buffer);

	                        // Because our block size is way smaller than the audio
	                        // buffer, we get blocks in bursts, which messes up
	                        // the audio analyzer.  We don't want to be forced to
	                        // wait until the analysis is done, because if
	                        // the analysis is slow, lag will build up.  Instead
	                        // wait, but with a timeout which lets us keep the
	                        // input serviced.
	                        long etime = System.currentTimeMillis();
	                        long sleep = sleepTime - (etime - stime);
	                        if (sleep < 5)
	                            sleep = 5;
	                        try {
	                            buffer.wait(sleep);
	                        } catch (InterruptedException e) {
	                        	e.printStackTrace();
	                        }
	                    }
	                }
	            }
	        } finally {
	            Log.i(TAG, "Reader: Stop Recording");
	            if (audioInput.getState() == AudioRecord.RECORDSTATE_RECORDING)
	                audioInput.stop();
	        }
	    }
	    
	    /**
	     * Notify the client that a read has completed.
	     * 
	     * @param   buffer      Buffer containing the data.
	     */
	    private void readDone(short[] buffer) {
	        inputListener.onReadComplete(buffer);
	    }
	    
	    /**
	     * Notify the client that an error has occurred.  The reader has been
	     * terminated.
	     * 
	     * @param   error       ERR_XXX code describing the error.
	     */
	    private void readError(int code) {
	        inputListener.onReadError(code);
	    }
	    
}
